/*
 * Copyright (c) 2012, the Dart project authors.
 * 
 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.dart.tools.search.ui.text;

import com.google.dart.tools.search.internal.ui.CopyToClipboardAction;
import com.google.dart.tools.search.internal.ui.SearchPlugin;
import com.google.dart.tools.search.internal.ui.SearchPluginImages;
import com.google.dart.tools.search.internal.ui.SelectAllAction;
import com.google.dart.tools.search.internal.ui.text.EditorOpener;
import com.google.dart.tools.search.ui.IContextMenuConstants;
import com.google.dart.tools.search.ui.IQueryListener;
import com.google.dart.tools.search.ui.ISearchQuery;
import com.google.dart.tools.search.ui.ISearchResult;
import com.google.dart.tools.search.ui.ISearchResultListener;
import com.google.dart.tools.search.ui.ISearchResultPage;
import com.google.dart.tools.search.ui.ISearchResultViewPart;
import com.google.dart.tools.search.ui.NewSearchUI;
import com.google.dart.tools.search.ui.SearchResultEvent;
import com.google.dart.tools.search2.internal.ui.InternalSearchUI;
import com.google.dart.tools.search2.internal.ui.SearchMessages;
import com.google.dart.tools.search2.internal.ui.SearchView;
import com.google.dart.tools.search2.internal.ui.basic.views.CollapseAllAction;
import com.google.dart.tools.search2.internal.ui.basic.views.ExpandAllAction;
import com.google.dart.tools.search2.internal.ui.basic.views.INavigate;
import com.google.dart.tools.search2.internal.ui.basic.views.SetLayoutAction;
import com.google.dart.tools.search2.internal.ui.basic.views.ShowNextResultAction;
import com.google.dart.tools.search2.internal.ui.basic.views.ShowPreviousResultAction;
import com.google.dart.tools.search2.internal.ui.basic.views.TableViewerNavigator;
import com.google.dart.tools.search2.internal.ui.basic.views.TreeViewerNavigator;
import com.google.dart.tools.search2.internal.ui.text.AnnotationManagers;
import com.google.dart.tools.search2.internal.ui.text.PositionTracker;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.util.OpenStrategy;
import org.eclipse.jface.viewers.DecoratingLabelProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.OpenAndLinkWithEditorHelper;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.part.IPageSite;
import org.eclipse.ui.part.Page;
import org.eclipse.ui.part.PageBook;
import org.eclipse.ui.progress.UIJob;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * An abstract base implementation for classes showing <code>AbstractTextSearchResult</code>
 * instances. This class assumes that the input element set via
 * {@link AbstractTextSearchViewPage#setInput(ISearchResult,Object)} is a subclass of
 * {@link AbstractTextSearchResult}. This result page supports a tree and/or a table presentation of
 * search results. Subclasses can determine which presentations they want to support at construction
 * time by passing the appropriate flags. Subclasses must customize the viewers for each
 * presentation with a label provider and a content provider. <br>
 * Changes in the search result are handled by updating the viewer in the
 * <code>elementsChanged()</code> and <code>clear()</code> methods.
 */
public abstract class AbstractTextSearchViewPage extends Page implements ISearchResultPage {
  private class SelectionProviderAdapter implements ISelectionProvider, ISelectionChangedListener {
    private ArrayList<ISelectionChangedListener> fListeners = new ArrayList<ISelectionChangedListener>(
        5);

    @Override
    public void addSelectionChangedListener(ISelectionChangedListener listener) {
      fListeners.add(listener);
    }

    @Override
    public ISelection getSelection() {
      return fViewer.getSelection();
    }

    @Override
    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
      fListeners.remove(listener);
    }

    @Override
    public void selectionChanged(SelectionChangedEvent event) {
      // forward to my listeners
      SelectionChangedEvent wrappedEvent = new SelectionChangedEvent(this, event.getSelection());
      for (Iterator<ISelectionChangedListener> listeners = fListeners.iterator(); listeners.hasNext();) {
        ISelectionChangedListener listener = listeners.next();
        listener.selectionChanged(wrappedEvent);
      }
    }

    @Override
    public void setSelection(ISelection selection) {
      fViewer.setSelection(selection);
    }

  }

  private class UpdateUIJob extends UIJob {

    public UpdateUIJob() {
      super(SearchMessages.AbstractTextSearchViewPage_update_job_name);
      setSystem(true);
    }

    /*
     * Undocumented for testing only. Used to find UpdateUIJobs.
     */
    @Override
    public boolean belongsTo(Object family) {
      return family == AbstractTextSearchViewPage.this;
    }

    @Override
    public IStatus runInUIThread(IProgressMonitor monitor) {
      Control control = getControl();
      if (control == null || control.isDisposed()) {
        // disposed the control while the UI was posted.
        return Status.OK_STATUS;
      }
      runBatchedClear();
      runBatchedUpdates();
      if (hasMoreUpdates() || isQueryRunning()) {
        schedule(500);
      } else {
        fIsUIUpdateScheduled = false;
        turnOnDecoration();
        updateBusyLabel();
        updateActionStates();
        if (fScheduleEnsureSelection) {
          fScheduleEnsureSelection = false;
          AbstractTextSearchResult result = getInput();
          if (result != null && fViewer.getSelection().isEmpty()) {
            navigateNext(true);
          }
        }
      }
      if (fViewPart != null) {
        fViewPart.updateLabel();
      }
      return Status.OK_STATUS;
    }

  }

  private volatile boolean fIsUIUpdateScheduled = false;

  private volatile boolean fScheduleEnsureSelection = false;
  private static final String KEY_LAYOUT = "com.google.dart.tools.search.resultpage.layout"; //$NON-NLS-1$
  /**
   * An empty array.
   */
  protected static final Match[] EMPTY_MATCH_ARRAY = new Match[0];

  private StructuredViewer fViewer;

  private Composite fViewerContainer;
  private Control fBusyLabel;
  private PageBook fPagebook;
  private boolean fIsBusyShown;
  private ISearchResultViewPart fViewPart;
  private Set<Object> fBatchedUpdates;
  private boolean fBatchedClearAll;
  private ISearchResultListener fListener;

  private IQueryListener fQueryListener;
  private MenuManager fMenu;
  private AbstractTextSearchResult fInput;
  // Actions
  private CopyToClipboardAction fCopyToClipboardAction;
  private Action fShowNextAction;
  private Action fShowPreviousAction;
  private ExpandAllAction fExpandAllAction;

  private CollapseAllAction fCollapseAllAction;
  private SetLayoutAction fFlatAction;

  private SetLayoutAction fHierarchicalAction;
  private int fCurrentLayout;
  private int fCurrentMatchIndex = 0;
  private String fId;
  private final int fSupportedLayouts;
  private SelectionProviderAdapter fViewerAdapter;
  private SelectAllAction fSelectAllAction;
  private Integer fElementLimit;

  /**
   * The editor opener.
   */
  private EditorOpener fEditorOpener = new EditorOpener();

  /**
   * Flag (<code>value 1</code>) denoting flat list layout.
   */
  public static final int FLAG_LAYOUT_FLAT = 1;

  /**
   * Flag (<code>value 2</code>) denoting tree layout.
   */
  public static final int FLAG_LAYOUT_TREE = 2;

  /**
   * Constructs this page with the default layout flags.
   * 
   * @see AbstractTextSearchViewPage#AbstractTextSearchViewPage(int)
   */
  protected AbstractTextSearchViewPage() {
    this(FLAG_LAYOUT_FLAT | FLAG_LAYOUT_TREE);
  }

  /**
   * This constructor must be passed a combination of layout flags combined with bitwise or. At
   * least one flag must be passed in (i.e. 0 is not a permitted value).
   * 
   * @param supportedLayouts flags determining which layout options this page supports. Must not be
   *          0
   * @see #FLAG_LAYOUT_FLAT
   * @see #FLAG_LAYOUT_TREE
   */
  protected AbstractTextSearchViewPage(int supportedLayouts) {
    fSupportedLayouts = supportedLayouts;
    initLayout();
    fShowNextAction = new ShowNextResultAction(this);
    fShowPreviousAction = new ShowPreviousResultAction(this);
    fCopyToClipboardAction = new CopyToClipboardAction();
    if ((supportedLayouts & FLAG_LAYOUT_TREE) != 0) {
      fExpandAllAction = new ExpandAllAction();
      fCollapseAllAction = new CollapseAllAction();
    }

    fSelectAllAction = new SelectAllAction();
    createLayoutActions();
    fBatchedUpdates = new HashSet<Object>();
    fBatchedClearAll = false;

    fListener = new ISearchResultListener() {
      @Override
      public void searchResultChanged(SearchResultEvent e) {
        handleSearchResultChanged(e);
      }
    };
    fElementLimit = null;
  }

  @Override
  public void createControl(Composite parent) {
    fQueryListener = createQueryListener();
    fMenu = new MenuManager("#PopUp"); //$NON-NLS-1$
    fMenu.setRemoveAllWhenShown(true);
    fMenu.setParent(getSite().getActionBars().getMenuManager());
    fMenu.addMenuListener(new IMenuListener() {
      @Override
      public void menuAboutToShow(IMenuManager mgr) {
        SearchView.createContextMenuGroups(mgr);
        fillContextMenu(mgr);
        fViewPart.fillContextMenu(mgr);
      }
    });
    fPagebook = new PageBook(parent, SWT.NULL);
    fPagebook.setLayoutData(new GridData(GridData.FILL_BOTH));
    fBusyLabel = createBusyControl();
    fViewerContainer = new Composite(fPagebook, SWT.NULL);
    fViewerContainer.setLayoutData(new GridData(GridData.FILL_BOTH));
    fViewerContainer.setSize(100, 100);
    fViewerContainer.setLayout(new FillLayout());

    fViewerAdapter = new SelectionProviderAdapter();
    getSite().setSelectionProvider(fViewerAdapter);
    // Register menu
    if (fViewPart != null) {
      getSite().registerContextMenu(fViewPart.getViewSite().getId(), fMenu, fViewerAdapter);
    }

    createViewer(fViewerContainer, fCurrentLayout);
    showBusyLabel(fIsBusyShown);
    NewSearchUI.addQueryListener(fQueryListener);

  }

  @Override
  public void dispose() {
    AbstractTextSearchResult oldSearch = getInput();
    if (oldSearch != null) {
      AnnotationManagers.removeSearchResult(getSite().getWorkbenchWindow(), oldSearch);
    }
    super.dispose();
    NewSearchUI.removeQueryListener(fQueryListener);
  }

  @Override
  public Control getControl() {
    return fPagebook;
  }

  /**
   * Returns the currently selected match.
   * 
   * @return the selected match or <code>null</code> if none are selected
   */
  public Match getCurrentMatch() {
    Object element = getFirstSelectedElement();
    if (element != null) {
      Match[] matches = getDisplayedMatches(element);
      if (fCurrentMatchIndex >= 0 && fCurrentMatchIndex < matches.length) {
        return matches[fCurrentMatchIndex];
      }
    }
    return null;
  }

  /**
   * Returns the current location of the match. This takes possible modifications of the file into
   * account. Therefore the result may differ from the position information that can be obtained
   * directly off the match.
   * 
   * @param match the match to get the position for.
   * @return the current position of the match.
   */
  public IRegion getCurrentMatchLocation(Match match) {
    PositionTracker tracker = InternalSearchUI.getInstance().getPositionTracker();

    int offset, length;
    Position pos = tracker.getCurrentPosition(match);
    if (pos == null) {
      offset = match.getOffset();
      length = match.getLength();
    } else {
      offset = pos.getOffset();
      length = pos.getLength();
    }
    return new Region(offset, length);
  }

  /**
   * Returns the number of matches that are currently displayed for the given element. If
   * {@link AbstractTextSearchResult#getActiveMatchFilters()} is not null, only matches are returned
   * that are not filtered by the match filters. Any action operating on the visible matches in the
   * search result page should use this method to get the match count for a search result (instead
   * of asking the search result directly).
   * 
   * @param element The element to get the matches for
   * @return The number of matches displayed for the given element. If the current input of this
   *         page is <code>null</code>, 0 is returned
   * @see AbstractTextSearchResult#getMatchCount(Object)
   */
  public int getDisplayedMatchCount(Object element) {
    AbstractTextSearchResult result = getInput();
    if (result == null) {
      return 0;
    }
    if (result.getActiveMatchFilters() == null) {
      return result.getMatchCount(element);
    }

    int count = 0;
    Match[] matches = result.getMatches(element);
    for (int i = 0; i < matches.length; i++) {
      if (!matches[i].isFiltered()) {
        count++;
      }
    }
    return count;
  }

  /**
   * Returns the matches that are currently displayed for the given element. If
   * {@link AbstractTextSearchResult#getActiveMatchFilters()} is not null, only matches are returned
   * that are not filtered by the match filters. If
   * {@link AbstractTextSearchResult#getActiveMatchFilters()} is null all matches of the given
   * element are returned. Any action operating on the visible matches in the search result page
   * should use this method to get the matches for a search result (instead of asking the search
   * result directly).
   * 
   * @param element The element to get the matches for
   * @return The matches displayed for the given element. If the current input of this page is
   *         <code>null</code>, an empty array is returned
   * @see AbstractTextSearchResult#getMatches(Object)
   */
  public Match[] getDisplayedMatches(Object element) {
    AbstractTextSearchResult result = getInput();
    if (result == null) {
      return EMPTY_MATCH_ARRAY;
    }
    Match[] matches = result.getMatches(element);
    if (result.getActiveMatchFilters() == null) {
      return matches;
    }

    int count = 0;
    for (int i = 0; i < matches.length; i++) {
      if (matches[i].isFiltered()) {
        matches[i] = null;
      } else {
        count++;
      }
    }
    if (count == matches.length) {
      return matches;
    }

    Match[] filteredMatches = new Match[count];
    for (int i = 0, k = 0; i < matches.length; i++) {
      if (matches[i] != null) {
        filteredMatches[k++] = matches[i];
      }
    }
    return filteredMatches;
  }

  /**
   * Gets the maximal number of top level elements to be shown in a viewer. <code>null</code> means
   * the view page does not limit the elements and will not provide UI to configure it. If a
   * non-null value is set, configuration UI will be provided. The limit value must be a positive
   * number or <code>-1</code> to not limit top level element.
   * 
   * @return returns the element limit. Valid values are:
   *         <dl>
   *         <li><code>null</code> to not limit and not provide configuration UI (default value)</li>
   *         <li><code>-1</code> to not limit and provide configuration UI</li>
   *         <li><code>positive integer</code> to limit by the given value and provide configuration
   *         UI</li>
   *         </dl>
   */
  public Integer getElementLimit() {
    return fElementLimit;
  }

  @Override
  public String getID() {
    return fId;
  }

  /**
   * Returns the currently shown result.
   * 
   * @return the previously set result or <code>null</code>
   * @see AbstractTextSearchViewPage#setInput(ISearchResult, Object)
   */
  public AbstractTextSearchResult getInput() {
    return fInput;
  }

  @Override
  public String getLabel() {
    AbstractTextSearchResult result = getInput();
    if (result == null) {
      return ""; //$NON-NLS-1$
    }
    return result.getLabel();
  }

  /**
   * Return the layout this page is currently using.
   * 
   * @return the layout this page is currently using
   * @see #FLAG_LAYOUT_FLAT
   * @see #FLAG_LAYOUT_TREE
   */
  public int getLayout() {
    return fCurrentLayout;
  }

  @Override
  public Object getUIState() {
    return fViewer.getSelection();
  }

  /**
   * Selects the element corresponding to the next match and shows the match in an editor. Note that
   * this will cycle back to the first match after the last match.
   */
  public void gotoNextMatch() {
    gotoNextMatch(false);
  }

  /**
   * Selects the element corresponding to the previous match and shows the match in an editor. Note
   * that this will cycle back to the last match after the first match.
   */
  public void gotoPreviousMatch() {
    gotoPreviousMatch(false);
  }

  @Override
  public void init(IPageSite pageSite) {
    super.init(pageSite);
    initActionDefinitionIDs();
    pageSite.getActionBars().updateActionBars();
  }

  /**
   * Note: this is internal API and should not be called from clients outside of the search plug-in.
   * <p>
   * Removes the currently selected match. Does nothing if no match is selected.
   * </p>
   * 
   * @noreference This method is not intended to be referenced by clients.
   */
  public void internalRemoveSelected() {
    AbstractTextSearchResult result = getInput();
    if (result == null) {
      return;
    }
    StructuredViewer viewer = getViewer();
    IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();

    HashSet<Match> set = new HashSet<Match>();
    if (viewer instanceof TreeViewer) {
      ITreeContentProvider cp = (ITreeContentProvider) viewer.getContentProvider();
      collectAllMatchesBelow(result, set, cp, selection.toArray());
    } else {
      collectAllMatches(set, selection.toArray());
    }
    navigateNext(true);

    Match[] matches = new Match[set.size()];
    set.toArray(matches);
    result.removeMatches(matches);
  }

  /**
   * Determines whether a certain layout is supported by this search result page.
   * 
   * @param layout the layout to test for
   * @return whether the given layout is supported or not
   * @see AbstractTextSearchViewPage#AbstractTextSearchViewPage(int)
   */
  public boolean isLayoutSupported(int layout) {
    return (layout & fSupportedLayouts) == layout;
  }

  @Override
  public void restoreState(IMemento memento) {
    if (countBits(fSupportedLayouts) > 1) {
      try {
        fCurrentLayout = getSettings().getInt(KEY_LAYOUT);
        // workaround because the saved value may be 0
        if (fCurrentLayout == 0) {
          initLayout();
        }
      } catch (NumberFormatException e) {
        // ignore, signals no value stored.
      }
      if (memento != null) {
        Integer layout = memento.getInteger(KEY_LAYOUT);
        if (layout != null) {
          fCurrentLayout = layout.intValue();
          // workaround because the saved value may be 0
          if (fCurrentLayout == 0) {
            initLayout();
          }
        }
      }
    }
  }

  @Override
  public void saveState(IMemento memento) {
    if (countBits(fSupportedLayouts) > 1) {
      memento.putInteger(KEY_LAYOUT, fCurrentLayout);
    }
  }

  /**
   * Sets the maximal number of top level elements to be shown in a viewer. If <code>null</code> is
   * set, the view page does not support to limit the elements and will not provide UI to configure
   * it. If a non-null value is set, configuration UI will be provided. The limit value must be a
   * positive number or <code>-1</code> to not limit top level element. If enabled, the element
   * limit has to be enforced by the content provider that is implemented by the client. The view
   * page just manages the value and configuration.
   * 
   * @param limit the element limit. Valid values are:
   *          <dl>
   *          <li><code>null</code> to not limit and not provide configuration UI</li>
   *          <li><code>-1</code> to not limit and provide configuration UI</li>
   *          <li><code>positive integer</code> to limit by the given value and provide
   *          configuration UI</li>
   *          </dl>
   */
  public void setElementLimit(Integer limit) {
    fElementLimit = limit;

    if (fViewer != null) {
      fViewer.refresh();
    }
    if (fViewPart != null) {
      fViewPart.updateLabel();
    }
  }

  @Override
  public void setFocus() {
    Control control = fViewer.getControl();
    if (control != null && !control.isDisposed()) {
      control.setFocus();
    }
  }

  @Override
  public void setID(String id) {
    fId = id;
  }

  @Override
  public void setInput(ISearchResult newSearch, Object viewState) {
    if (newSearch != null && !(newSearch instanceof AbstractTextSearchResult)) {
      return; // ignore
    }

    AbstractTextSearchResult oldSearch = fInput;
    if (oldSearch != null) {
      disconnectViewer();
      oldSearch.removeListener(fListener);
      AnnotationManagers.removeSearchResult(getSite().getWorkbenchWindow(), oldSearch);
    }
    fInput = (AbstractTextSearchResult) newSearch;

    if (fInput != null) {
      AnnotationManagers.addSearchResult(getSite().getWorkbenchWindow(), fInput);

      fInput.addListener(fListener);
      connectViewer(fInput);
      if (viewState instanceof ISelection) {
        fViewer.setSelection((ISelection) viewState, true);
      } else {
        navigateNext(true);
      }

      updateBusyLabel();
      turnOffDecoration();
      scheduleUIUpdate();

    } else {
      if (getViewPart() != null) {
        getViewPart().updateLabel();
      }
    }
  }

  /**
   * Sets the layout of this search result page. The layout must be on of
   * <code>FLAG_LAYOUT_FLAT</code> or <code>FLAG_LAYOUT_TREE</code> and it must be one of the values
   * passed during construction of this search result page.
   * 
   * @param layout the new layout
   * @see AbstractTextSearchViewPage#isLayoutSupported(int)
   */
  public void setLayout(int layout) {
    Assert.isTrue(countBits(layout) == 1);
    Assert.isTrue(isLayoutSupported(layout));
    if (countBits(fSupportedLayouts) < 2) {
      return;
    }
    if (fCurrentLayout == layout) {
      return;
    }
    fCurrentLayout = layout;
    ISelection selection = fViewer.getSelection();
    disconnectViewer();
    disposeViewer();
    createViewer(fViewerContainer, layout);
    fViewerContainer.layout(true);
    connectViewer(fInput);
    fViewer.setSelection(selection, true);
    getSettings().put(KEY_LAYOUT, layout);
    getViewPart().updateLabel();
  }

  /**
   * Sets the view part
   * 
   * @param part View part to set
   */
  @Override
  public void setViewPart(ISearchResultViewPart part) {
    fViewPart = part;
  }

  /**
   * Determines whether the provided selection can be used to remove matches from the result.
   * 
   * @param selection the selection to test
   * @return returns <code>true</code> if the elements in the current selection can be removed.
   */
  protected boolean canRemoveMatchesWith(ISelection selection) {
    return !selection.isEmpty();
  }

  /**
   * This method is called whenever all elements have been removed from the shown
   * <code>AbstractSearchResult</code>. This method is guaranteed to be called in the UI thread.
   * Note that this notification is asynchronous. i.e. further changes may have occurred by the time
   * this method is called. They will be described in a future call.
   */
  protected abstract void clear();

  /**
   * Configures the given viewer. Implementers have to set at least a content provider and a label
   * provider. This method may be called if the page was constructed with the flag
   * <code>FLAG_LAYOUT_FLAT</code>.
   * 
   * @param viewer the viewer to be configured
   */
  protected abstract void configureTableViewer(TableViewer viewer);

  /**
   * Configures the given viewer. Implementers have to set at least a content provider and a label
   * provider. This method may be called if the page was constructed with the flag
   * <code>FLAG_LAYOUT_TREE</code>.
   * 
   * @param viewer the viewer to be configured
   */
  protected abstract void configureTreeViewer(TreeViewer viewer);

  /**
   * Creates the table viewer to be shown on this page. Clients may override this method.
   * 
   * @param parent the parent widget
   * @return returns a newly created <code>TableViewer</code>
   */
  protected TableViewer createTableViewer(Composite parent) {
    return new TableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
  }

  /**
   * Creates the tree viewer to be shown on this page. Clients may override this method.
   * 
   * @param parent the parent widget
   * @return returns a newly created <code>TreeViewer</code>.
   */
  protected TreeViewer createTreeViewer(Composite parent) {
    return new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
  }

  /**
   * This method is called whenever the set of matches for the given elements changes. This method
   * is guaranteed to be called in the UI thread. Note that this notification is asynchronous. i.e.
   * further changes may have occurred by the time this method is called. They will be described in
   * a future call.
   * <p>
   * The changed elements are evaluated by {@link #evaluateChangedElements(Match[], Set)}.
   * </p>
   * 
   * @param objects array of objects that has to be refreshed
   */
  protected abstract void elementsChanged(Object[] objects);

  /**
   * Evaluates the elements to that are later passed to {@link #elementsChanged(Object[])}. By
   * default the element to change are the elements received by ({@link Match#getElement()}). Client
   * implementations can modify this behavior.
   * 
   * @param matches the matches that were added or removed
   * @param changedElements the set that collects the elements to change. Clients should only add
   *          elements to the set.
   */
  protected void evaluateChangedElements(Match[] matches, Set<Object> changedElements) {
    for (int i = 0; i < matches.length; i++) {
      changedElements.add(matches[i].getElement());
    }
  }

  /**
   * Fills the context menu for this page. Subclasses may override this method.
   * 
   * @param mgr the menu manager representing the context menu
   */
  protected void fillContextMenu(IMenuManager mgr) {

    if (getMatchCount() > 0) {
      mgr.appendToGroup(IContextMenuConstants.GROUP_SHOW, fShowNextAction);
      mgr.appendToGroup(IContextMenuConstants.GROUP_SHOW, fShowPreviousAction);
    }
    if (!getViewer().getSelection().isEmpty()) {
      mgr.appendToGroup(IContextMenuConstants.GROUP_EDIT, fCopyToClipboardAction);
    }
    if (getMatchCount() > 0) {
      if (getLayout() == FLAG_LAYOUT_TREE) {
        mgr.appendToGroup(IContextMenuConstants.GROUP_SHOW, fExpandAllAction);
      }
    }
  }

  /**
   * Fills the toolbar contribution for this page. Subclasses may override this method.
   * 
   * @param tbm the tool bar manager representing the view's toolbar
   */
  protected void fillToolbar(IToolBarManager tbm) {
    tbm.appendToGroup(IContextMenuConstants.GROUP_OPEN, fShowNextAction);
    tbm.appendToGroup(IContextMenuConstants.GROUP_OPEN, fShowPreviousAction);
    IActionBars actionBars = getSite().getActionBars();
    if (actionBars != null) {
      actionBars.setGlobalActionHandler(ActionFactory.NEXT.getId(), fShowNextAction);
      actionBars.setGlobalActionHandler(ActionFactory.PREVIOUS.getId(), fShowPreviousAction);
      actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), fCopyToClipboardAction);
      actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), fSelectAllAction);
    }
    if (getLayout() == FLAG_LAYOUT_TREE) {
      tbm.appendToGroup(IContextMenuConstants.GROUP_VIEWER_SETUP, fExpandAllAction);
      tbm.appendToGroup(IContextMenuConstants.GROUP_VIEWER_SETUP, fCollapseAllAction);
    }
  }

  /**
   * Returns a dialog settings object for this search result page. There will be one dialog settings
   * object per search result page id.
   * 
   * @return the dialog settings for this search result page
   * @see AbstractTextSearchViewPage#getID()
   */
  protected IDialogSettings getSettings() {
    IDialogSettings parent = SearchPlugin.getDefault().getDialogSettings();
    IDialogSettings settings = parent.getSection(getID());
    if (settings == null) {
      settings = parent.addNewSection(getID());
    }
    return settings;
  }

  /**
   * Returns the viewer currently used in this page.
   * 
   * @return the currently used viewer or <code>null</code> if none has been created yet.
   */
  protected StructuredViewer getViewer() {
    return fViewer;
  }

  /**
   * Returns the view part set with <code>setViewPart(ISearchResultViewPart)</code>.
   * 
   * @return The view part or <code>null</code> if the view part hasn't been set yet (or set to
   *         null).
   */
  protected ISearchResultViewPart getViewPart() {
    return fViewPart;
  }

  /**
   * <p>
   * This method is called when the search page gets an 'open' event from its underlying viewer (for
   * example on double click). The default implementation will open the first match on any element
   * that has matches. If the element to be opened is an inner node in the tree layout, the node
   * will be expanded if it's collapsed and vice versa. Subclasses are allowed to override this
   * method.
   * </p>
   * 
   * @param event the event sent for the currently shown viewer
   * @see IOpenListener
   */
  protected void handleOpen(OpenEvent event) {
    Viewer viewer = event.getViewer();
    boolean hasCurrentMatch = showCurrentMatch(OpenStrategy.activateOnOpen());
    ISelection sel = event.getSelection();
    if (viewer instanceof TreeViewer && sel instanceof IStructuredSelection) {
      IStructuredSelection selection = (IStructuredSelection) sel;
      TreeViewer tv = (TreeViewer) getViewer();
      Object element = selection.getFirstElement();
      if (element != null) {
        if (!hasCurrentMatch && getDisplayedMatchCount(element) > 0) {
          gotoNextMatch(OpenStrategy.activateOnOpen());
        } else {
          tv.setExpandedState(element, !tv.getExpandedState(element));
        }
      }
      return;
    } else if (!hasCurrentMatch) {
      gotoNextMatch(OpenStrategy.activateOnOpen());
    }
  }

  /**
   * Handles a search result event for the current search result.
   * 
   * @param e the event to handle
   */
  protected void handleSearchResultChanged(final SearchResultEvent e) {
    if (e instanceof MatchEvent) {
      postUpdate(((MatchEvent) e).getMatches());
    } else if (e instanceof RemoveAllEvent) {
      postClear();
    } else if (e instanceof FilterUpdateEvent) {
      postUpdate(((FilterUpdateEvent) e).getUpdatedMatches());
    }
  }

  /**
   * Opens an editor on the given file resource.
   * <p>
   * If the page already has an editor open on the target object then that editor is brought to
   * front; otherwise, a new editor is opened. If <code>activate == true</code> the editor will be
   * activated.
   * <p>
   * 
   * @param page the workbench page in which the editor will be opened
   * @param file the file to open
   * @param activate if <code>true</code> the editor will be activated
   * @return an open editor or <code>null</code> if an external editor was opened
   * @throws PartInitException if the editor could not be initialized
   * @see org.eclipse.ui.IWorkbenchPage#openEditor(IEditorInput, String, boolean)
   */
  protected final IEditorPart open(IWorkbenchPage page, IFile file, boolean activate)
      throws PartInitException {
    return fEditorOpener.open(page, file, activate);
  }

  /**
   * Opens an editor on the given file resource and tries to select the given offset and length.
   * <p>
   * If the page already has an editor open on the target object then that editor is brought to
   * front; otherwise, a new editor is opened. If <code>activate == true</code> the editor will be
   * activated.
   * <p>
   * 
   * @param page the workbench page in which the editor will be opened
   * @param file the file to open
   * @param offset the offset to select in the editor
   * @param length the length to select in the editor
   * @param activate if <code>true</code> the editor will be activated
   * @return an open editor or <code>null</code> if an external editor was opened
   * @throws PartInitException if the editor could not be initialized
   * @see org.eclipse.ui.IWorkbenchPage#openEditor(IEditorInput, String, boolean)
   */
  protected final IEditorPart openAndSelect(IWorkbenchPage page, IFile file, int offset,
      int length, boolean activate) throws PartInitException {
    return fEditorOpener.openAndSelect(page, file, offset, length, activate);
  }

  /**
   * Posts a UI update to make sure an element is selected.
   */
  protected void postEnsureSelection() {
    fScheduleEnsureSelection = true;
    scheduleUIUpdate();
  }

  /**
   * Opens an editor on the given element and selects the given range of text. If a search results
   * implements a <code>IFileMatchAdapter</code>, match locations will be tracked and the current
   * match range will be passed into this method.
   * 
   * @param match the match to show
   * @param currentOffset the current start offset of the match
   * @param currentLength the current length of the selection
   * @throws PartInitException if an editor can't be opened
   * @see org.eclipse.core.filebuffers.ITextFileBufferManager
   * @see IFileMatchAdapter
   * @deprecated Use {@link #showMatch(Match, int, int, boolean)} instead
   */
  @Deprecated
  protected void showMatch(Match match, int currentOffset, int currentLength)
      throws PartInitException {
  }

  /**
   * Opens an editor on the given element and selects the given range of text. If a search results
   * implements a <code>IFileMatchAdapter</code>, match locations will be tracked and the current
   * match range will be passed into this method. If the <code>activate</code> parameter is
   * <code>true</code> the opened editor should have be activated. Otherwise the focus should not be
   * changed.
   * 
   * @param match the match to show
   * @param currentOffset the current start offset of the match
   * @param currentLength the current length of the selection
   * @param activate whether to activate the editor.
   * @throws PartInitException if an editor can't be opened
   * @see org.eclipse.core.filebuffers.ITextFileBufferManager
   * @see IFileMatchAdapter
   */
  protected void showMatch(Match match, int offset, int length, boolean activate)
      throws PartInitException {
    IWorkbenchPage page = getSite().getPage();
    fEditorOpener.openAndSelect(page, match, offset, length, activate);
  }

  protected void updateActionStates() {
    boolean matches = getMatchCount() > 0;
    fShowNextAction.setEnabled(matches);
    fShowPreviousAction.setEnabled(matches);
    fExpandAllAction.setEnabled(matches);
    fCollapseAllAction.setEnabled(matches);
  }

  private void asyncExec(final Runnable runnable) {
    final Control control = getControl();
    if (control != null && !control.isDisposed()) {
      Display currentDisplay = Display.getCurrent();
      if (currentDisplay == null || !currentDisplay.equals(control.getDisplay())) {
        // meaning we're not executing on the display thread of the
        // control
        control.getDisplay().asyncExec(new Runnable() {
          @Override
          public void run() {
            if (!control.isDisposed()) {
              runnable.run();
            }
          }
        });
      } else {
        runnable.run();
      }
    }
  }

  private void collectAllMatches(HashSet<Match> set, Object[] elements) {
    for (int j = 0; j < elements.length; j++) {
      Match[] matches = getDisplayedMatches(elements[j]);
      for (int i = 0; i < matches.length; i++) {
        set.add(matches[i]);
      }
    }
  }

  private void collectAllMatchesBelow(AbstractTextSearchResult result, Set<Match> set,
      ITreeContentProvider cp, Object[] elements) {
    for (int j = 0; j < elements.length; j++) {
      Match[] matches = getDisplayedMatches(elements[j]);
      for (int i = 0; i < matches.length; i++) {
        set.add(matches[i]);
      }
      Object[] children = cp.getChildren(elements[j]);
      collectAllMatchesBelow(result, set, cp, children);
    }
  }

  private void connectViewer(AbstractTextSearchResult search) {
    fViewer.setInput(search);
  }

  private int countBits(int layoutFlags) {
    int bitCount = 0;
    for (int i = 0; i < 32; i++) {
      if (layoutFlags % 2 == 1) {
        bitCount++;
      }
      layoutFlags >>= 1;
    }
    return bitCount;
  }

  private Control createBusyControl() {
    Table busyLabel = new Table(fPagebook, SWT.NONE);
    TableItem item = new TableItem(busyLabel, SWT.NONE);
    item.setText(SearchMessages.AbstractTextSearchViewPage_searching_label);
    busyLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    return busyLabel;
  }

  private void createLayoutActions() {
    if (countBits(fSupportedLayouts) > 1) {
      fFlatAction = new SetLayoutAction(
          this,
          SearchMessages.AbstractTextSearchViewPage_flat_layout_label,
          SearchMessages.AbstractTextSearchViewPage_flat_layout_tooltip,
          FLAG_LAYOUT_FLAT);
      fHierarchicalAction = new SetLayoutAction(
          this,
          SearchMessages.AbstractTextSearchViewPage_hierarchical_layout_label,
          SearchMessages.AbstractTextSearchViewPage_hierarchical_layout_tooltip,
          FLAG_LAYOUT_TREE);
      SearchPluginImages.setImageDescriptors(
          fFlatAction,
          SearchPluginImages.T_LCL,
          SearchPluginImages.IMG_LCL_SEARCH_FLAT_LAYOUT);
      SearchPluginImages.setImageDescriptors(
          fHierarchicalAction,
          SearchPluginImages.T_LCL,
          SearchPluginImages.IMG_LCL_SEARCH_HIERARCHICAL_LAYOUT);
    }
  }

  private IQueryListener createQueryListener() {
    return new IQueryListener() {
      @Override
      public void queryAdded(ISearchQuery query) {
        // ignore
      }

      @Override
      public void queryFinished(final ISearchQuery query) {
        // handle the end of the query in the UIUpdateJob, as ui updates
        // may not be finished here.
        postEnsureSelection();
      }

      @Override
      public void queryRemoved(ISearchQuery query) {
        // ignore
      }

      @Override
      public void queryStarting(final ISearchQuery query) {
        final Runnable runnable1 = new Runnable() {
          @Override
          public void run() {
            updateBusyLabel();
            AbstractTextSearchResult result = getInput();

            if (result == null || !result.getQuery().equals(query)) {
              return;
            }
            turnOffDecoration();
            scheduleUIUpdate();
          }

        };
        asyncExec(runnable1);
      }
    };
  }

  private void createViewer(Composite parent, int layout) {
    if ((layout & FLAG_LAYOUT_FLAT) != 0) {
      TableViewer viewer = createTableViewer(parent);
      fViewer = viewer;
      configureTableViewer(viewer);
    } else if ((layout & FLAG_LAYOUT_TREE) != 0) {
      TreeViewer viewer = createTreeViewer(parent);
      fViewer = viewer;
      configureTreeViewer(viewer);
      fCollapseAllAction.setViewer(viewer);
      fExpandAllAction.setViewer(viewer);
    }

    fCopyToClipboardAction.setViewer(fViewer);
    fSelectAllAction.setViewer(fViewer);

    IToolBarManager tbm = getSite().getActionBars().getToolBarManager();
    tbm.removeAll();
    SearchView.createToolBarGroups(tbm);
    fillToolbar(tbm);
    tbm.update(false);

    new OpenAndLinkWithEditorHelper(fViewer) {

      @Override
      protected void activate(ISelection selection) {
        final int currentMode = OpenStrategy.getOpenMethod();
        try {
          OpenStrategy.setOpenMethod(OpenStrategy.DOUBLE_CLICK);
          handleOpen(new OpenEvent(fViewer, selection));
        } finally {
          OpenStrategy.setOpenMethod(currentMode);
        }
      }

      @Override
      protected void linkToEditor(ISelection selection) {
        // not supported by this part
      }

      @Override
      protected void open(ISelection selection, boolean activate) {
        handleOpen(new OpenEvent(fViewer, selection));
      }

    };

    fViewer.addSelectionChangedListener(new ISelectionChangedListener() {
      @Override
      public void selectionChanged(SelectionChangedEvent event) {
        fCurrentMatchIndex = -1;
      }
    });

    fViewer.addSelectionChangedListener(fViewerAdapter);

    Menu menu = fMenu.createContextMenu(fViewer.getControl());
    fViewer.getControl().setMenu(menu);

    updateLayoutActions();
    if (getViewPart() != null) {
      getViewPart().updateLabel();
    }
  }

  private void disconnectViewer() {
    fViewer.setInput(null);
  }

  private void disposeViewer() {
    fViewer.removeSelectionChangedListener(fViewerAdapter);
    fViewer.getControl().dispose();
    fViewer = null;
  }

  private Object getFirstSelectedElement() {
    IStructuredSelection selection = (IStructuredSelection) fViewer.getSelection();
    if (selection.size() > 0) {
      return selection.getFirstElement();
    }
    return null;
  }

  private int getMatchCount() {
    AbstractTextSearchResult result = getInput();
    if (result == null) {
      return 0;
    }
    return result.getMatchCount();
  }

  private void gotoNextMatch(boolean activateEditor) {
    fCurrentMatchIndex++;
    Match nextMatch = getCurrentMatch();
    if (nextMatch == null) {
      navigateNext(true);
      fCurrentMatchIndex = 0;
    }
    showCurrentMatch(activateEditor);
  }

  private void gotoPreviousMatch(boolean activateEditor) {
    fCurrentMatchIndex--;
    Match nextMatch = getCurrentMatch();
    if (nextMatch == null) {
      navigateNext(false);
      fCurrentMatchIndex = getDisplayedMatchCount(getFirstSelectedElement()) - 1;
    }
    showCurrentMatch(activateEditor);
  }

  private synchronized boolean hasMoreUpdates() {
    return fBatchedClearAll || fBatchedUpdates.size() > 0;
  }

  private void initActionDefinitionIDs() {
    fCopyToClipboardAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_COPY);
// filtered out by a greedy activity
//    fShowNextAction.setActionDefinitionId(IWorkbenchCommandConstants.NAVIGATE_NEXT);
//    fShowPreviousAction.setActionDefinitionId(IWorkbenchCommandConstants.NAVIGATE_PREVIOUS);
    fSelectAllAction.setActionDefinitionId(IWorkbenchCommandConstants.EDIT_SELECT_ALL);
  }

  private void initLayout() {
    if (supportsTreeLayout()) {
      fCurrentLayout = FLAG_LAYOUT_TREE;
    } else {
      fCurrentLayout = FLAG_LAYOUT_FLAT;
    }
  }

  private boolean isQueryRunning() {
    AbstractTextSearchResult result = getInput();
    if (result != null) {
      return NewSearchUI.isQueryRunning(result.getQuery());
    }
    return false;
  }

  private void navigateNext(boolean forward) {
    INavigate navigator = null;
    if (fViewer instanceof TableViewer) {
      navigator = new TableViewerNavigator((TableViewer) fViewer);
    } else {
      navigator = new TreeViewerNavigator(this, (TreeViewer) fViewer);
    }
    navigator.navigateNext(forward);
  }

  private synchronized void postClear() {
    fBatchedClearAll = true;
    fBatchedUpdates.clear();
    scheduleUIUpdate();
  }

  private synchronized void postUpdate(Match[] matches) {
    evaluateChangedElements(matches, fBatchedUpdates);
    scheduleUIUpdate();
  }

  private void runBatchedClear() {
    synchronized (this) {
      if (!fBatchedClearAll) {
        return;
      }
      fBatchedClearAll = false;
      updateBusyLabel();
    }
    getViewPart().updateLabel();
    clear();
  }

  private synchronized void runBatchedUpdates() {
    elementsChanged(fBatchedUpdates.toArray());
    fBatchedUpdates.clear();
    updateBusyLabel();
  }

  private synchronized void scheduleUIUpdate() {
    if (!fIsUIUpdateScheduled) {
      fIsUIUpdateScheduled = true;
      new UpdateUIJob().schedule();
    }
  }

  private void showBusyLabel(boolean shouldShowBusy) {
    if (shouldShowBusy) {
      fPagebook.showPage(fBusyLabel);
    } else {
      fPagebook.showPage(fViewerContainer);
    }
  }

  private boolean showCurrentMatch(boolean activateEditor) {
    Match currentMatch = getCurrentMatch();
    if (currentMatch != null) {
      showMatch(currentMatch, activateEditor);
      return true;
    }
    return false;
  }

  private void showMatch(final Match match, final boolean activateEditor) {
    ISafeRunnable runnable = new ISafeRunnable() {
      @Override
      public void handleException(Throwable exception) {
        if (exception instanceof PartInitException) {
          PartInitException pie = (PartInitException) exception;
          ErrorDialog.openError(
              getSite().getShell(),
              SearchMessages.DefaultSearchViewPage_show_match,
              SearchMessages.DefaultSearchViewPage_error_no_editor,
              pie.getStatus());
        }
      }

      @Override
      public void run() throws Exception {
        IRegion location = getCurrentMatchLocation(match);
        showMatch(match, location.getOffset(), location.getLength(), activateEditor);
      }
    };
    SafeRunner.run(runnable);
  }

  private boolean supportsTreeLayout() {
    return isLayoutSupported(FLAG_LAYOUT_TREE);
  }

  private void turnOffDecoration() {
    IBaseLabelProvider lp = fViewer.getLabelProvider();
    if (lp instanceof DecoratingLabelProvider) {
      ((DecoratingLabelProvider) lp).setLabelDecorator(null);
    }
  }

  private void turnOnDecoration() {
    IBaseLabelProvider lp = fViewer.getLabelProvider();
    if (lp instanceof DecoratingLabelProvider) {
      ((DecoratingLabelProvider) lp).setLabelDecorator(PlatformUI.getWorkbench().getDecoratorManager().getLabelDecorator());

    }
  }

  private void updateBusyLabel() {
    AbstractTextSearchResult result = getInput();
    boolean shouldShowBusy = result != null && NewSearchUI.isQueryRunning(result.getQuery())
        && result.getMatchCount() == 0;
    if (shouldShowBusy == fIsBusyShown) {
      return;
    }
    fIsBusyShown = shouldShowBusy;
    showBusyLabel(fIsBusyShown);
  }

  private void updateLayoutActions() {
    if (fFlatAction != null) {
      fFlatAction.setChecked(fCurrentLayout == fFlatAction.getLayout());
    }
    if (fHierarchicalAction != null) {
      fHierarchicalAction.setChecked(fCurrentLayout == fHierarchicalAction.getLayout());
    }
  }

}
